'use strict'

//切换工作目录(这样无论在哪里开启工作区，都会将工作区跳转到当前目录下)
process.chdir(__dirname);

const titbit=require('titbit');
const fs=require('fs');
const dbcfg=require('./dbconfig.js');  //引入数据库配置文件
const pg=require('pg');  //创建连接池需要引入的库
const wxkey=require('./wxkey.js');
const {httpcli}=require('gohttp'); //封装了http请求模块的请求库，使用它里面的httpcli向微信服务器端发送请求获取openid
const token=require('titbit-token');  //使用它构造加密对象
const {cors} = require('titbit-toolkit');  //使用它来解决跨域问题

try{
    //检查该目录下是否存在images文件夹
    fs.accessSync('./images');
}catch(err){
    fs.mkdirSync('./images');
}

//创建一个连接池，连接池是服务器和数据库之间进行交互的桥梁
let psql=new pg.Pool(dbcfg);

let cert_path='/usr/local/share';

//new一个titbit对象时，就是执行了http.createServer()，开启一个服务
const app=new titbit({
    //开启测试模式，若有错误会输出错误信息
    debug:true,
    globalLog:true,  //开启全局日志
    cert:`${cert_path}/api.lzjlw.top.pem`,
    key:`${cert_path}/api.lzjlw.top.key`
})

//解决跨域问题
app.pre(new cors)

//给get请求添加'/'路由
app.get('/',async c=>{
    c.send('ok')  //用来设置c.res.body(是经过封装的一个属性，它会将数据自动返回)的数据
                  //c.res-->一个对象包括encoding、body属性，用来暂存返回数据的编码和具体数据
})
/* 注意：send函数只是设置ctx.res.body属性的值。
   和直接进行ctx.res.body赋值没有区别，只是因为函数调用如果出错会更快发现问题，
   而设置属性值写错了就是添加了一个新的属性，不会报错但是请求不会返回正确的数据。*/

app.post('/',async c=>{
    c.send(c.body);  //c.body-->请求体的数据
})

/** -------------------------content路由---------------------------- */

//获取内容列表
app.get('/content',async c=>{
    //通过psql连接池去数据库端拿取数据
    //psql只对外提供query这一个接口，第一个参数为要进行的操作的字符串，第二个参数为需要传入的值，如果不需要传入值，就不用写
    let ret=await psql.query(`select * from content`);
    c.send(ret.rows);  //ret.rows为query中的操作获取到的数据
    //不需要psql.end(),因为客户端请求不断，就需要服务器不断地从数据库获取数据，所以连接池不能关
})

//获取具体内容
app.get('/content/:id',async c=>{
    let sqltext='SELECT * FROM content WHERE id=$1';
    let r=await psql.query(sqltext,[c.param.id]);
    if(r.rowCount==0){  //r.rowCount表示查询到的数据的条数
        return c.status(404).send('content not found')
    }
    c.send(r.rows[0]);
})

//创建内容
app.post('/content',async c=>{
    try{
        //提交的Body数据通过从c.body获取
        let data=JSON.parse(c.body);
        let sqltext=`INSERT INTO content(id,add_time,detail,title,update_time) VALUES($1,$2,$3,$4,$5)`;

        //生成唯一ID
        let id=c.helper.makeId();//在连续大量的获取一个随机数时保证生成不一样的数据
        let tm=Date.now();
        let r=await psql.query(sqltext,[id,tm,data.detail,data.title,tm]);
        if(r.rowCount==0){ //r.rowCount表示成功插入数据的条数
            return c.status(404).send('failed');
        }
        c.send(id);
    }catch(err){
        c.status(404).send('it is not json data')
    }
})

app.delete('/content/:id',async c=>{
    try{
        let delId=c.param.id;
        let sqltext=`delete from content where id=$1`;
        let r=await psql.query(sqltext,[delId]);
        if(r.rowCount==1){
            c.send(`delete ${delId} success -- ${r.rowCount}`);
        }
    }catch(err){
        c.status(404).send('delete failed');
    }
})
/** -------------------------content路由-END-------------------------- */

app.get('/upload',async c=>{
    //请求上下文c(一个封装了各种请求数据的对象),c的helper模块里有很多函数，其中有一个pipe函数
    //c.reply是响应对象，可写的流，
    //在http/1.1协议里指向response；在http/2协议里指向stream
    await c.helper.pipe('./upload.html',c.reply)
})

app.post('/upload',async c=>{
    //根据name获取文件对象
    let f=c.getFile('image')  //获取上传的文件信息，其实就是读取files属性的信息    c.files-->上传文件保存的信息

    if(!f){
        return c.status(400).send('image not found');
    }

    //根据传入的原始文件名生成唯一文件名,主要使用f.filename的扩展名,默认为时间字符串+随机数：2021-01-01_12-12-23_27789.jpg
    let fname=c.helper.makeName(f.filename);

    //移动文件到指定位置
    await c.moveFile(f,`./images/${fname}`);

    c.send(fname);
})

//添加返回图片数据的路由
/* 在该titbit框架中':'为用来识别动态变化的参数的符号
  /image/固定，:name就是一个可变参数(且该参数必须是一级的，即参数中不能有'/')，只要发送的请求的url前面是'/image/'就符合要求 */
app.get('/image/:name',async c=>{
    //c.param是一个对象，其中的name属性值为name可变参数的值
    let imgname=c.param.name;

    await c.helper.pipe(`./images/${imgname}`,c.reply);
})

/* ------------------------------------------------------------------------------------------------ */

let tok=new token({
    //token有效期，单位为秒
    expires:45, //加密后的数据生成后，只能在生成后的规定时限内进行解密，否则会超时造成解密失败
    //加解密的密钥，必须是32位字符
    key:'1234rdfvgt6yhu7ij7yhg8jngfruikng',
    //初始向量值，用于和最开始的数据块进行运算，必须是16位字符(该值最好是随机生成的)
    iv:'6tyghvui984rf7jy'
})

app.get('/mp-login/:code',async c=>{
    //auth_url是开发者服务器向微信服务器发送登录凭证校验的网址
    let auth_url=`https://api.weixin.qq.com/sns/jscode2session?appid=${wxkey.appid}&secret=${wxkey.secret}&js_code=${c.param.code}&grant_type=authorization_code`;
    let ret=await httpcli.get(auth_url)  //开发者服务器端作为客户端向微信服务器端发送请求
                        .then(res=>{  //res是微信服务器端给开发者服务器端返回的结果，包括openid等
                            return res.json();
                        })
    
    c.send(tok.make(ret));  //将从微信服务器端拿到的结果进行加密处理后传给小程序，进行加密的数据(ret)必须是json对象
})

//该路由只用于进行解密结果展示
app.get('/decrypt/:token',async c=>{
    console.log(tok.verify(c.param.token)); //解密后的结果是对象，经过浏览器的处理后变成了json字符串
    c.send(tok.verify(c.param.token));  //进行解密处理
})
/* 结果示例：
    解密后的结果：{
        ok: true,
        data: {
            session_key: 'XR1WPWexFuQTFDSEEvgwZg==',
            openid: 'oJOXp5H4hKFGS-dPdekWIkJ_HtD8',
            expires: 45000,
            timestamp: 1634029825459,
            tokenId: ''
        },
        now: 1634029843614
    }
    呈现在浏览器上的结果：
    {"ok":true,"data":{"session_key":"S/NLzzciQ5zIByNlWrS2sA==","openid":"oJOXp5H4hKFGS-dPdekWIkJ_HtD8",
    "expires":45000,"timestamp":1633941628402,"tokenId":""},"now":1633941638241} */

//使用pre接口添加的中间件和use添加的主要区别就是会在接收body数据之前执行。可用于在接收数据之前的权限过滤操作
// app.pre(async (c, next) => {
//     //在每次请求过来之后先进行权限判断
//     /* 每次发送请求时，在请求头中加上authorization属性，将从微信服务器端获取到的加密后的数据赋给该属性
//        或者将该加密数据加在请求的参数中，开发者服务器在收到请求后先进行路由的匹配，有匹配项后，先进行下面用户权限的判断
//        获取请求中的加密数据，进行解密，解密的结果中有一个ok属性，若该值为true则表示用户拥有权限；若为false则表示用户没有权限 */
//     let token = c.headers.authorization || c.query.token  //c.query是url传递的参数，即?后面的参数
//     if (!token) {
//         return c.status(403).send('deny')
//     }

//     let r = tok.verify(token)

//     if (r.ok === false) {
//         return c.status(403).send(r.errcode)
//     }

//     //c.box--默认为空对象，可以添加任何属性值，用来动态传递给下一层组件需要使用的信息。
//     c.box.user = r.data

//     await next()  //调用下一层组件

// })

/* 文件对象
{
    filename,   原始文件名
    'content-type',   文件类型
    start,   ctx.rawBody开始的索引位置
    end,   ctx.rawBody结束的索引位置   
    length,   文件长度，字节数
    rawHeader   原始消息头文本，是multipart/form-data的消息头
}
*/

/**HTTP消息头的content-type指定了数据的类型(数据的格式)
   Client                                               Server
   content-type描述的值要和提交的body数据的格式一致       根据content-type的值决定如何解析body数据
   content-type:
   multipart/form-data;boundary=xxx   -------------    文件解析
   text/*          --------------------------------    文本类型
   application/*   --------------------------------    要看具体值，application/json也是文本类型
   .......         --------------------------------    每种类型如何处理是双方设计好的通信方式
   到web框架这一层面，该如何处理是开发框架设计好的，但通常都要遵循一个原则：允许开发者做自己的处理
   在本框架范围内：
   multipart类型 --- c.body是object类型，文件信息解析到c.files
   application/x-www-form-urlencoded  传统的表单类型 --- c.body是object类型，用于获取提交的表单数据
   text/* --- c.body 是文本类型，c.body = c.rawBody.toString('utf8')
   其他类型： c.body 是buffer类型，就是对c.rawBody的引用
 */

//运行在2345端口
app.run(2345)
